﻿using LightCAD.MathLib;
using System;
using System.Collections.Generic;

namespace LightCAD.Three
{
    public class WebGLLights
    {
        public sealed class LightUniformValues : JsObj<object>
        {
            private Vector3 _position;
            public Vector3 position
            {
                get => _position;
                set
                {
                    set("position", value);
                }
            }

            private Vector3 _direction;
            public Vector3 direction
            {
                get => _direction;
                set
                {
                    set("direction", value);
                }
            }

            private Color _color;
            public Color color
            {
                get => _color;
                set
                {
                    set("color", value);
                }
            }

            private Color _skyColor;
            public Color skyColor
            {
                get => _skyColor;
                set
                {
                    set("skyColor", value);
                }
            }

            private Color _groundColor;
            public Color groundColor
            {
                get => _groundColor;
                set
                {
                    set("groundColor", value);
                }
            }

            private double? _distance;
            public object distance
            {
                get => _distance;
                set
                {
                    set("distance", value);
                }
            }

            private double? _decay;
            public object decay
            {
                get => _decay;
                set
                {
                    set("decay", value);
                }
            }

            private double? _coneCos;
            public object coneCos
            {
                get => _coneCos;
                set
                {
                    set("coneCos", value);
                }
            }

            private double? _penumbraCos;
            public object penumbraCos
            {
                get => _penumbraCos;
                set
                {
                    set("penumbraCos", value);
                }
            }

            private Vector3 _halfWidth;
            public Vector3 halfWidth
            {
                get => _halfWidth;
                set
                {
                    set("halfWidth", value);
                }
            }

            private Vector3 _halfHeight;
            public Vector3 halfHeight
            {
                get => _halfHeight;
                set
                {
                    set("halfHeight", value);
                }
            }

            public override JsObj<object> set(string name, object value)
            {
                if (value is int)
                    value = Convert.ToDouble(value);
                switch (name)
                {
                    case "position":
                        _position = value as Vector3;
                        break;
                    case "direction":
                        _direction = value as Vector3;
                        break;
                    case "color":
                        _color = value as Color;
                        break;
                    case "skyColor":
                        _skyColor = value as Color;
                        break;
                    case "groundColor":
                        _groundColor = value as Color;
                        break;
                    case "distance":
                        _distance = (double?)value;
                        break;
                    case "decay":
                        _decay = (double?)value;
                        break;
                    case "coneCos":
                        _coneCos = (double?)value;
                        break;
                    case "penumbraCos":
                        _penumbraCos = (double?)value;
                        break;
                    case "halfWidth":
                        _halfWidth = value as Vector3;
                        break;
                    case "halfHeight":
                        _halfHeight = value as Vector3;
                        break;
                    default:
                        break;
                }
                return base.set(name, value);
            }
            public override JsObj<object> remove(string name)
            {
                switch (name)
                {
                    case "position":
                        _position = null;
                        break;
                    case "direction":
                        _direction = null;
                        break;
                    case "color":
                        _color = null;
                        break;
                    case "skyColor":
                        _skyColor = null;
                        break;
                    case "groundColor":
                        _groundColor = null;
                        break;
                    case "distance":
                        _distance = null;
                        break;
                    case "decay":
                        _decay = null;
                        break;
                    case "coneCos":
                        _coneCos = null;
                        break;
                    case "penumbraCos":
                        _penumbraCos = null;
                        break;
                    case "halfWidth":
                        _halfWidth = null;
                        break;
                    case "halfHeight":
                        _halfHeight = null;
                        break;
                    default:
                        break;
                }
                return base.remove(name);
            }

            public override void Clear()
            {
                this._position = null;
                this._direction = null;
                this._color = null;
                this._skyColor = null;
                this._groundColor = null;
                this._distance = null;
                this._decay = null;
                this._coneCos = null;
                this._penumbraCos = null;
                this._halfWidth = null;
                this._halfHeight = null;
                base.Clear();
            }
        }
        public class UniformsCache
        {
            private readonly JsObj<int, LightUniformValues> lights;

            public UniformsCache()
            {
                this.lights = new JsObj<int, LightUniformValues>();
            }

            public LightUniformValues get(Light light)
            {

                if (lights[light.id] != null)
                {

                    return lights[light.id];

                }
                LightUniformValues uniforms = null;
                switch (light.type)
                {

                    case "DirectionalLight":
                        uniforms = new LightUniformValues {
                                { "direction", new Vector3()},
                                {"color", new Color() }

                            };
                        break;
                    case "SpotLight":
                        uniforms = new LightUniformValues {
                                { "position", new Vector3() },
                                { "direction", new Vector3()},
                                { "color", new Color()},
                                { "distance", 0},
                                { "coneCos", 0},
                                { "penumbraCos", 0},
                                { "decay", 0}
                            };
                        break;

                    case "PointLight":
                        uniforms = new LightUniformValues {
                                {"position", new Vector3()},
                                {"color", new Color()},
                                {"distance", 0},
                                {"decay", 0},

                            };
                        break;

                    case "HemisphereLight":
                        uniforms = new LightUniformValues {
                                {"direction", new Vector3()},
                                {"skyColor", new Color()},
                                { "groundColor", new Color()}

                            };
                        break;

                    case "RectAreaLight":
                        uniforms = new LightUniformValues {
                                {"color", new Color() },
                                {"position", new Vector3()},
                                {"halfWidth", new Vector3()},
                                {"halfHeight", new Vector3() }
                            };
                        break;
                }

                lights[light.id] = uniforms;

                return uniforms;

            }
        }

        public sealed class LightShadowUniformValues : JsObj<object>
        {
            private double? _shadowBias;
            public object shadowBias
            {
                get => _shadowBias;
                set
                {
                    set("shadowBias", value);
                }
            }

            private double? _shadowNormalBias;
            public object shadowNormalBias
            {
                get => _shadowNormalBias;
                set
                {
                    set("shadowNormalBias", value);
                }
            }

            private double? _shadowRadius;
            public object shadowRadius
            {
                get => _shadowRadius;
                set
                {
                    set("shadowRadius", value);
                }
            }

            private Vector2 _shadowMapSize;
            public object shadowMapSize
            {
                get => _shadowMapSize;
                set
                {
                    set("shadowMapSize", value);
                }
            }

            private double? _shadowCameraNear;
            public object shadowCameraNear
            {
                get => _shadowCameraNear;
                set
                {
                    set("shadowCameraNear", value);
                }
            }

            private double? _shadowCameraFar;
            public object shadowCameraFar
            {
                get => _shadowCameraFar;
                set
                {
                    set("shadowCameraFar", value);
                }
            }
            public override JsObj<object> set(string name, object value)
            {
                if ((value is int))
                    value = Convert.ToDouble(value);
                switch (name)
                {
                    case "shadowBias":
                        _shadowBias = (double?)value;
                        break;
                    case "shadowNormalBias":
                        _shadowNormalBias = (double?)value;
                        break;
                    case "shadowRadius":
                        _shadowRadius = (double?)value;
                        break;
                    case "shadowMapSize":
                        _shadowMapSize = value as Vector2;
                        break;
                    case "shadowCameraNear":
                        _shadowCameraNear = (double?)value;
                        break;
                    case "shadowCameraFar":
                        _shadowCameraFar = (double?)value;
                        break;
                    default:
                        break;
                }
                return base.set(name, value);
            }
            public override JsObj<object> remove(string name)
            {
                double? value = null;
                switch (name)
                {
                    case "shadowBias":
                        _shadowBias = value;
                        break;
                    case "shadowNormalBias":
                        _shadowNormalBias = value;
                        break;
                    case "shadowRadius":
                        _shadowRadius = value;
                        break;
                    case "shadowMapSize":
                        _shadowMapSize = null;
                        break;
                    case "shadowCameraNear":
                        _shadowCameraNear = value;
                        break;
                    case "shadowCameraFar":
                        _shadowCameraFar = value;
                        break;
                    default:
                        break;
                }
                return base.remove(name);
            }

            public override void Clear()
            {
                this._shadowBias = null;
                this._shadowNormalBias = null;
                this._shadowRadius = null;
                this._shadowMapSize = null;
                this._shadowCameraNear = null;
                this._shadowCameraFar = null;

                base.Clear();
            }
        }
        public class ShadowUniformsCache
        {

            private readonly JsObj<int, LightShadowUniformValues> lights = new JsObj<int, LightShadowUniformValues>();

            public LightShadowUniformValues get(Light light)
            {

                if (lights[light.id] != null)
                {

                    return lights[light.id];

                }

                LightShadowUniformValues uniforms = null;

                switch (light.type)
                {

                    case "DirectionalLight":
                        uniforms = new LightShadowUniformValues {
                                {"shadowBias", 0 },
                                {"shadowNormalBias", 0},
                                {"shadowRadius", 1},
                                {"shadowMapSize", new Vector2()},
                    };
                        break;

                    case "SpotLight":
                        uniforms = new LightShadowUniformValues{
                                {"shadowBias", 0},
                                {"shadowNormalBias", 0},
                                {"shadowRadius", 1},
                                { "shadowMapSize", new Vector2()},

                    };
                        break;

                    case "PointLight":
                        uniforms = new LightShadowUniformValues {
                                { "shadowBias", 0},
                                { "shadowNormalBias", 0},
                                { "shadowRadius", 1},
                                { "shadowMapSize", new Vector2()},
                                { "shadowCameraNear", 1},
                                { "shadowCameraFar", 1000},

                    };
                        break;

                        // TODO (abelnation): set RectAreaLight shadow uniforms

                }

                lights[light.id] = uniforms;

                return uniforms;

            }


        }
        int nextVersion = 0;

        public static int shadowCastingAndTexturingLightsFirst(Light lightA, Light lightB)
        {

            var mapA = lightA is ILightMap ? (lightA as ILightMap).getMap() : null;
            var mapB = lightB is ILightMap ? (lightB as ILightMap).getMap() : null;
            return (lightB.castShadow ? 2 : 0) - (lightA.castShadow ? 2 : 0) + (mapB != null ? 1 : 0) - (mapA != null ? 1 : 0);

        }
        public sealed class Hash
        {
            public int directionalLength = -1;
            public int pointLength = -1;
            public int spotLength = -1;
            public int rectAreaLength = -1;
            public int hemiLength = -1;
            public int numDirectionalShadows = -1;
            public int numPointShadows = -1;
            public int numSpotShadows = -1;
            public int numSpotMaps = -1;
        }
        public sealed class State
        {
            public int version = 0;

            public Hash hash = new Hash
            {
                directionalLength = -1,
                pointLength = -1,
                spotLength = -1,
                rectAreaLength = -1,
                hemiLength = -1,

                numDirectionalShadows = -1,
                numPointShadows = -1,
                numSpotShadows = -1,
                numSpotMaps = -1
            };

            public ListEx<double> ambient = new ListEx<double> { 0, 0, 0 };
            public ListEx<Vector3> probe = new ListEx<Vector3>();

            public ListEx<LightUniformValues> point = new ListEx<LightUniformValues>();
            public ListEx<LightShadowUniformValues> pointShadow = new ListEx<LightShadowUniformValues>();
            public ListEx<Texture> pointShadowMap = new ListEx<Texture>();
            public ListEx<Matrix4> pointShadowMatrix = new ListEx<Matrix4>();

            public ListEx<LightUniformValues> directional = new ListEx<LightUniformValues>();
            public ListEx<LightShadowUniformValues> directionalShadow = new ListEx<LightShadowUniformValues>();
            public ListEx<Texture> directionalShadowMap = new ListEx<Texture>();
            public ListEx<Matrix4> directionalShadowMatrix = new ListEx<Matrix4>();

            public ListEx<LightUniformValues> spot = new ListEx<LightUniformValues>();
            public ListEx<LightShadowUniformValues> spotShadow = new ListEx<LightShadowUniformValues>();
            public ListEx<Texture> spotLightMap = new ListEx<Texture>();
            public ListEx<Texture> spotShadowMap = new ListEx<Texture>();
            public ListEx<Matrix4> spotLightMatrix = new ListEx<Matrix4>();

            public ListEx<LightUniformValues> hemi = new ListEx<LightUniformValues>();

            public ListEx<LightUniformValues> rectArea = new ListEx<LightUniformValues>();

            public DataTexture rectAreaLTC1 = null;
            public DataTexture rectAreaLTC2 = null;



            public int numSpotLightShadowsWithMaps = 0;
        }
        private WebGLExtensions extensions;
        private WebGLCapabilities capabilities;
        private readonly UniformsCache cache;
        private readonly ShadowUniformsCache shadowCache;
        public readonly State state;
        private readonly Vector3 vector3;
        private readonly Matrix4 matrix4;
        private readonly Matrix4 matrix42;
        public WebGLLights(WebGLExtensions extensions, WebGLCapabilities capabilities)
        {
            this.extensions = extensions;
            this.capabilities = capabilities;
            this.cache = new UniformsCache();
            this.shadowCache = new ShadowUniformsCache();
            this.state = new State();
            for (var i = 0; i < 9; i++) state.probe.Push(new Vector3());
            this.vector3 = new Vector3();
            this.matrix4 = new Matrix4();
            this.matrix42 = new Matrix4();
        }
        public void setup(ListEx<Light> lights, bool useLegacyLights)
        {

            double r = 0, g = 0, b = 0;

            for (var i = 0; i < 9; i++) state.probe[i].Set(0, 0, 0);

            var directionalLength = 0;
            var pointLength = 0;
            var spotLength = 0;
            var rectAreaLength = 0;
            var hemiLength = 0;

            var numDirectionalShadows = 0;
            var numPointShadows = 0;
            var numSpotShadows = 0;
            var numSpotMaps = 0;
            var numSpotShadowsWithMaps = 0;

            // ordering : [shadow casting + map texturing, map texturing, shadow casting, none ]
            lights.Sort(shadowCastingAndTexturingLightsFirst);

            // artist-friendly light intensity scaling factor
            var scaleFactor = (useLegacyLights) ? Math.PI : 1;

            for (int i = 0, l = lights.Length; i < l; i++)
            {

                var light = lights[i];

                var color = light.color;
                var intensity = light.intensity;
                var distance = (light is ILightDistance) ? (light as ILightDistance).getDistance() : 0;
                LightShadow lightShadow = null;
                if (light is ILightShadow)
                    lightShadow = (light as ILightShadow).getShadow();
                var shadowMap = (lightShadow != null && lightShadow.map != null) ? lightShadow.map.texture : null;

                if (light is AmbientLight)
                {

                    r += color.R * intensity * scaleFactor;
                    g += color.G * intensity * scaleFactor;
                    b += color.B * intensity * scaleFactor;

                }
                else if (light is LightProbe)
                {
                    var probeLight = light as LightProbe;

                    for (var j = 0; j < 9; j++)
                    {

                        state.probe[j].AddScaledVector(probeLight.sh.Coefficients[j], intensity);

                    }

                }
                else if (light is DirectionalLight)
                {

                    var directLight = light as DirectionalLight;
                    var uniforms = cache.get(light);

                    (uniforms.color as Color).Copy(light.color).MultiplyScalar(light.intensity * scaleFactor);
                    if (light.castShadow)
                    {

                        var shadow = directLight.shadow;
                        var shadowUniforms = shadowCache.get(light);

                        shadowUniforms["shadowBias"] = shadow.bias;
                        shadowUniforms["shadowNormalBias"] = shadow.normalBias;
                        shadowUniforms["shadowRadius"] = shadow.radius;
                        shadowUniforms["shadowMapSize"] = shadow.mapSize;

                        state.directionalShadow[directionalLength] = shadowUniforms;
                        state.directionalShadowMap[directionalLength] = shadowMap;
                        state.directionalShadowMatrix[directionalLength] = directLight.shadow.matrix;

                        numDirectionalShadows++;

                    }

                    state.directional[directionalLength] = uniforms;

                    directionalLength++;

                }
                else if (light is SpotLight)
                {
                    var spotLight = light as SpotLight;
                    var uniforms = cache.get(light);

                    (uniforms.position as Vector3).SetFromMatrixPosition(light.matrixWorld);

                    uniforms.color.Copy(color).MultiplyScalar(intensity * scaleFactor);
                    uniforms.distance = distance;

                    uniforms.coneCos = Math.Cos(spotLight.angle);
                    uniforms.penumbraCos = Math.Cos(spotLight.angle * (1 - spotLight.penumbra));
                    uniforms.decay = spotLight.decay;
                    state.spot[spotLength] = uniforms;

                    var shadow = spotLight.shadow;

                    if (spotLight.map != null)
                    {

                        state.spotLightMap[numSpotMaps] = spotLight.map;
                        numSpotMaps++;

                        // make sure the lightMatrix is up to date
                        // TODO : do it if required only
                        shadow.updateMatrices(light);

                        if (light.castShadow) numSpotShadowsWithMaps++;
                    }

                    state.spotLightMatrix[spotLength] = shadow.matrix;

                    if (light.castShadow)
                    {

                        var shadowUniforms = shadowCache.get(light);

                        shadowUniforms.shadowBias = shadow.bias;
                        shadowUniforms.shadowNormalBias = shadow.normalBias;
                        shadowUniforms.shadowRadius = shadow.radius;
                        shadowUniforms.shadowMapSize = shadow.mapSize;

                        state.spotShadow[spotLength] = shadowUniforms;
                        state.spotShadowMap[spotLength] = shadowMap;

                        numSpotShadows++;

                    }

                    spotLength++;

                }
                else if (light is RectAreaLight)
                {
                    var rectAreaLight = light as RectAreaLight;
                    var uniforms = cache.get(light);

                    (uniforms.color as Color).Copy(color).MultiplyScalar(intensity);

                    uniforms.halfWidth.Set(rectAreaLight.width * 0.5, 0.0, 0.0);
                    uniforms.halfHeight.Set(0.0, rectAreaLight.height * 0.5, 0.0);

                    state.rectArea[rectAreaLength] = uniforms;

                    rectAreaLength++;

                }
                else if (light is PointLight)
                {
                    var pLight = light as PointLight;
                    var uniforms = cache.get(light);

                    (uniforms.color as Color).Copy(light.color).MultiplyScalar(light.intensity * scaleFactor);
                    uniforms.distance = pLight.distance;
                    uniforms.decay = pLight.decay;

                    if (light.castShadow)
                    {

                        var shadow = pLight.shadow;

                        var shadowUniforms = shadowCache.get(light);

                        shadowUniforms.shadowBias = shadow.bias;
                        shadowUniforms.shadowNormalBias = shadow.normalBias;
                        shadowUniforms.shadowRadius = shadow.radius;
                        shadowUniforms.shadowMapSize = shadow.mapSize;
                        shadowUniforms.shadowCameraNear = shadow.camera.near;
                        shadowUniforms.shadowCameraFar = shadow.camera.far;

                        state.pointShadow[pointLength] = shadowUniforms;
                        state.pointShadowMap[pointLength] = shadowMap;
                        state.pointShadowMatrix[pointLength] = shadow.matrix;

                        numPointShadows++;

                    }

                    state.point[pointLength] = uniforms;

                    pointLength++;

                }
                else if (light is HemisphereLight)
                {
                    var hLight = light as HemisphereLight;
                    var uniforms = cache.get(light);

                    uniforms.skyColor.Copy(light.color).MultiplyScalar(intensity * scaleFactor);
                    uniforms.groundColor.Copy(hLight.groundColor).MultiplyScalar(intensity * scaleFactor);

                    state.hemi[hemiLength] = uniforms;

                    hemiLength++;

                }

            }

            if (rectAreaLength > 0)
            {
                state.rectAreaLTC1 = UniformsLib.LTC_FLOAT_1;
                state.rectAreaLTC2 = UniformsLib.LTC_FLOAT_2;

            }

            state.ambient[0] = r;
            state.ambient[1] = g;
            state.ambient[2] = b;

            var hash = state.hash;

            if (hash.directionalLength != directionalLength ||
                hash.pointLength != pointLength ||
                hash.spotLength != spotLength ||
                hash.rectAreaLength != rectAreaLength ||
                hash.hemiLength != hemiLength ||
                hash.numDirectionalShadows != numDirectionalShadows ||
                hash.numPointShadows != numPointShadows ||
                hash.numSpotShadows != numSpotShadows ||
                hash.numSpotMaps != numSpotMaps)
            {

                state.directional.Length = directionalLength;
                state.spot.Length = spotLength;
                state.rectArea.Length = rectAreaLength;
                state.point.Length = pointLength;
                state.hemi.Length = hemiLength;

                state.directionalShadow.Length = numDirectionalShadows;
                state.directionalShadowMap.Length = numDirectionalShadows;
                state.pointShadow.Length = numPointShadows;
                state.pointShadowMap.Length = numPointShadows;
                state.spotShadow.Length = numSpotShadows;
                state.spotShadowMap.Length = numSpotShadows;
                state.directionalShadowMatrix.Length = numDirectionalShadows;
                state.pointShadowMatrix.Length = numPointShadows;
                state.spotLightMatrix.Length = numSpotShadows + numSpotMaps - numSpotShadowsWithMaps;
                state.spotLightMap.Length = numSpotMaps;
                state.numSpotLightShadowsWithMaps = numSpotShadowsWithMaps;

                hash.directionalLength = directionalLength;
                hash.pointLength = pointLength;
                hash.spotLength = spotLength;
                hash.rectAreaLength = rectAreaLength;
                hash.hemiLength = hemiLength;

                hash.numDirectionalShadows = numDirectionalShadows;
                hash.numPointShadows = numPointShadows;
                hash.numSpotShadows = numSpotShadows;
                hash.numSpotMaps = numSpotMaps;

                state.version = nextVersion++;

            }

        }

        public void setupView(ListEx<Light> lights, Camera camera)
        {

            var directionalLength = 0;
            var pointLength = 0;
            var spotLength = 0;
            var rectAreaLength = 0;
            var hemiLength = 0;

            var viewMatrix = camera.matrixWorldInverse;

            for (int i = 0, l = lights.Length; i < l; i++)
            {

                var light = lights[i];

                if (light is DirectionalLight)
                {

                    var uniforms = state.directional[directionalLength];

                    (uniforms.direction as Vector3).SetFromMatrixPosition(light.matrixWorld);
                    vector3.SetFromMatrixPosition(light.target.matrixWorld);
                    (uniforms.direction as Vector3).Sub(vector3);
                    (uniforms.direction as Vector3).TransformDirection(viewMatrix);
                    directionalLength++;

                }
                else if (light is SpotLight)
                {

                    var uniforms = state.spot[spotLength];

                    (uniforms.position as Vector3).SetFromMatrixPosition(light.matrixWorld);
                    (uniforms.position as Vector3).ApplyMatrix4(viewMatrix);

                    uniforms.direction.SetFromMatrixPosition(light.matrixWorld);
                    vector3.SetFromMatrixPosition(light.target.matrixWorld);
                    uniforms.direction.Sub(vector3);
                    uniforms.direction.TransformDirection(viewMatrix);

                    spotLength++;

                }
                else if (light is RectAreaLight)
                {
                    var raLight = light as RectAreaLight;
                    var uniforms = state.rectArea[rectAreaLength];

                    (uniforms.position as Vector3).SetFromMatrixPosition(light.matrixWorld);
                    (uniforms.position as Vector3).ApplyMatrix4(viewMatrix);

                    // extract local rotation of light to derive width/height half vectors
                    matrix42.Identity();
                    matrix4.Copy(light.matrixWorld);
                    matrix4.Premultiply(viewMatrix);
                    matrix42.ExtractRotation(matrix4);

                    uniforms.halfWidth.Set(raLight.width * 0.5, 0.0, 0.0);
                    uniforms.halfHeight.Set(0.0, raLight.height * 0.5, 0.0);

                    uniforms.halfWidth.ApplyMatrix4(matrix42);
                    uniforms.halfHeight.ApplyMatrix4(matrix42);

                    rectAreaLength++;

                }
                else if (light is PointLight)
                {
                    var pLight = light as PointLight;
                    var uniforms = state.point[pointLength];

                    (uniforms.position as Vector3).SetFromMatrixPosition(light.matrixWorld);
                    (uniforms.position as Vector3).ApplyMatrix4(viewMatrix);

                    pointLength++;

                }
                else if (light is HemisphereLight)
                {

                    var uniforms = state.hemi[hemiLength];

                    uniforms.direction.SetFromMatrixPosition(light.matrixWorld);
                    uniforms.direction.TransformDirection(viewMatrix);

                    hemiLength++;

                }

            }

        }
    }
}
